两种 Provider 的使用
你对 UI Automation 的 Client-Side Provider 和 Server-Side Provider 的了解非常正确。这两种方法确实可以为不支持 UI Automation 的应用添加自动化支持。要直接通过 COM 实现这些 Provider,需要一些复杂的步骤。我会尽量简化解释,并给出一些指导。
首先,让我们区分这两种 Provider:
-
Client-Side Provider:
- 运行在客户端进程中
- 不需要修改目标应用程序
- 通常用于为旧的或第三方应用程序添加 UI Automation 支持
-
Server-Side Provider:
- 运行在目标应用程序的进程中
- 需要修改目标应用程序的源代码
- 提供更精确的控制和更好的性能
实现步骤:
-
创建 COM 对象: 首先,你需要创建一个 COM 对象,实现相关的 UI Automation Provider 接口。
-
实现必要的接口: 根据你要支持的 UI 元素类型,实现相应的接口。例如:
- IRawElementProviderSimple
- IRawElementProviderFragment
- 特定控件类型的接口(如 IValueProvider, IInvokeProvider 等)
-
注册 COM 对象: 将你的 COM 对象注册到系统中,使 UI Automation 能够找到并使用它。
-
连接到目标应用:
- 对于 Client-Side Provider,你需要找到目标窗口并附加你的 Provider。
- 对于 Server-Side Provider,你需要在应用程序中创建和公开你的 Provider。
示例代码(C++):
这里是一个非常简化的 Client-Side Provider 示例:
#include <UIAutomationCore.h>
#include <atlbase.h>
#include <atlcom.h>
// 声明 COM 类
class ATL_NO_VTABLE CMyProvider :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CMyProvider, &CLSID_MyProvider>,
public IRawElementProviderSimple,
public IRawElementProviderFragment
{
public:
DECLARE_NOT_AGGREGATABLE(CMyProvider)
BEGIN_COM_MAP(CMyProvider)
COM_INTERFACE_ENTRY(IRawElementProviderSimple)
COM_INTERFACE_ENTRY(IRawElementProviderFragment)
END_COM_MAP()
DECLARE_PROTECT_FINAL_CONSTRUCT()
// IRawElementProviderSimple 方法
STDMETHOD(get_ProviderOptions)(ProviderOptions *pRetVal);
STDMETHOD(GetPatternProvider)(PATTERNID patternId, IUnknown **pRetVal);
STDMETHOD(GetPropertyValue)(PROPERTYID propertyId, VARIANT *pRetVal);
STDMETHOD(get_HostRawElementProvider)(IRawElementProviderSimple **pRetVal);
// IRawElementProviderFragment 方法
STDMETHOD(Navigate)(NavigateDirection direction, IRawElementProviderFragment **pRetVal);
STDMETHOD(GetRuntimeId)(SAFEARRAY **pRetVal);
STDMETHOD(get_BoundingRectangle)(UiaRect *pRetVal);
STDMETHOD(GetEmbeddedFragmentRoots)(SAFEARRAY **pRetVal);
STDMETHOD(SetFocus)();
STDMETHOD(get_FragmentRoot)(IRawElementProviderFragmentRoot **pRetVal);
// 其他方法...
};
OBJECT_ENTRY_AUTO(__uuidof(MyProvider), CMyProvider)
// 实现方法...
STDMETHODIMP CMyProvider::get_ProviderOptions(ProviderOptions *pRetVal)
{
*pRetVal = ProviderOptions_ClientSideProvider;
return S_OK;
}
// 实现其他方法...
要使用这个 Provider:
- 编译成 DLL 并注册为 COM 服务器。
- 在客户端代码中,找到目标窗口并创建 Provider 实例。
- 使用 UiaHostProviderFromHwnd 将 Provider 附加到窗口。
CComPtr<IRawElementProviderSimple> pProvider;
pProvider.CoCreateInstance(CLSID_MyProvider);
HWND hwndTarget = FindWindow(NULL, L"Target Window");
UiaHostProviderFromHwnd(hwndTarget, &pProvider);
注意事项:
- 这只是一个非常基础的示例。实际实现要复杂得多,需要处理更多细节。
- 直接使用 COM 实现 UI Automation Provider 需要深入了解 COM 编程和 UI Automation 框架。
- 对于 Java 或 Node.js 开发者,直接实现 COM 接口可能会很困难。你可能需要考虑使用 C++ 实现核心功能,然后通过 JNI (Java) 或 N-API (Node.js) 进行封装。
如果你主要使用 Java 或 Node.js,可能更适合使用现有的自动化库,如 Java 的 WindowsAccessBridge 或 Node.js 的 windows-automation 模块,这些库已经封装了许多底层细节。
不需要 Hook
你的理解是正确的。UiaHostProviderFromHwnd 函数确实不需要 hook 目标程序就能直接使用。这是 UI Automation 框架提供的一个强大特性,允许我们为现有的应用程序添加自 动化支持,而无需修改目标程序的代码或注入任何东西到目标进程中。
让我详细解释一下 UiaHostProviderFromHwnd 的工作原理和使用方式:
-
功能: UiaHostProviderFromHwnd 允许你为指定的窗口句柄(HWND)关联一个自定义的 UI Automation provider。
-
工作原理:
- 它在调用进程中创建一个 provider 到窗口的关联。
- 当 UI Automation 客户端查询该窗口时,你的自定义 provider 会被调用,而不是默认的 provider。
-
使用场景:
- 为不支持 UI Automation 的旧应用程序添加自动化支持。
- 为第三方应用程序扩展或自定义自动化功能。
-
不需要 Hook: 这个函数不需要修改目标程序或向其注入代码。它完全在调用进程中运行。
-
使用示例:
HRESULT AddAutomationSupport(HWND hwndTarget)
{
// 创建自定义 provider
CComPtr<IRawElementProviderSimple> pProvider;
HRESULT hr = pProvider.CoCreateInstance(CLSID_MyCustomProvider);
if (FAILED(hr))
return hr;
// 将 provider 关联到目标窗口
hr = UiaHostProviderFromHwnd(hwndTarget, pProvider);
if (FAILED(hr))
return hr;
return S_OK;
}
-
注意事项:
- provider 必须实现 IRawElementProviderSimple 接口。
- provider 通常还需要实现其他接口,如 IRawElementProviderFragment,以提供完整的功能。
- 这个方法创建的是一个 client-side provider,它运行在调用进程中,而不是目标应用程序进程中。
-
优点:
- 非侵入性:不需要修改目标程序。
- 灵活性:可以为任何窗口添加或自定义自动化支持。
- 安全:不涉及注入代码到其他进程。
-
限制:
- 由于 provider 运行在调用进程中,它可能无法访问目标应用程序的内部状态。
- 性能可能不如直接在目标应用程序中实现的 server-side provider。
-
适用于 Java 和 Node.js: 虽然 UiaHostProviderFromHwnd 是一个 Windows API 函数,主要用于 C/C++ 开发,但是:
- 对于 Java,你可以使用 JNA (Java Native Access) 来调用这个函数。
- 对于 Node.js,可以使用 node-ffi 模块来实现类似的功能。
总的来说,UiaHostProviderFromHwnd 提供了一种强大而灵活的方式来增强现有应用程序的自动化能力,而无需修改目标程序。这对于开发自动化工具、辅助技术或测试框架特别有用。
UiaHostProviderFromHwnd 的原理
你提 出了一个非常好的问题。确实,UiaHostProviderFromHwnd 的工作原理可能看起来有点神奇。让我详细解释一下它是如何工作的,以及为什么它能为没有实现 UI Automation 的程序提供这种能力。
UiaHostProviderFromHwnd 的工作原理:
-
Windows 消息系统:
- Windows 使用消息系统来管理窗口和控件。
- 每个窗口都有一个消息队列和一个窗口过程(Window Procedure)来处理消息。
-
UI Automation 的系统级集成:
- UI Automation 在系统级别集成到了 Windows 的消息系统中。
- 当 UI Automation 客户端请求信息时,系统会向目标窗口发送特定的消息。
-
UiaHostProviderFromHwnd 的角色:
- 这个函数告诉系统,对于指定的窗口句柄(HWND),应该使用提供的自定义 Provider 来响应 UI Automation 请求。
- 它实际上是在系统级别注册了一个回调机制。
-
消息拦截:
- 当 UI Automation 相关的消息被发送到目标窗口时,系统会先检查是否有注册的自定义 Provider。
- 如果有,系统会调用这个自定义 Provider,而不是将消息直接发送到目标窗口。
-
无需目标程序配合:
- 重要的是,这整个过程不需要目标程序的任何特殊处理或修改。
- 自定义 Provider 完全在调用进程中运行,不会影响到目标程序的代码执行。
-
限制和边界:
- 自定义 Provider 无法直接访问目标程序的内部状态。
- 它主要基于可以从外部观察到的窗口属性和行为来提供信息。
示例说明:
想象你有一个旧的计算器程序,它没有实现 UI Automation。你可以创建一个自定义 Provider:
class CalculatorProvider : public IRawElementProviderSimple
{
public:
HRESULT GetPropertyValue(PROPERTYID propertyId, VARIANT *pRetVal) override
{
if (propertyId == UIA_NamePropertyId)
{
pRetVal->vt = VT_BSTR;
pRetVal->bstrVal = SysAllocString(L"Calculator");
return S_OK;
}
// 处理其他属性...
return S_OK;
}
// 实现其他必要的接口方法...
};
// 使用方法
HWND hwndCalculator = FindWindow(L"CalculatorClass", NULL);
CComPtr<IRawElementProviderSimple> pProvider = new CalculatorProvider();
UiaHostProviderFromHwnd(hwndCalculator, pProvider);
这段代码做了以下几件事:
- 创建了一个自定义 Provider,它知道如何描述计算器窗口。
- 找到计算器窗口的句柄。
- 使用 UiaHostProviderFromHwnd 将自定义 Provider 与计算器窗口关联。
现在,当 UI Automation 客户端查询这个计算器窗口时:
- 系统会发现有一个自定义 Provider 注册在这个窗口上。
- 系统会调用你的 CalculatorProvider 来获取信息,而不是直接查询计算器程序。
- 你的 Provider 可以提供你认为合适的信息,即使原始程序并不支持 UI Automation。
总结: UiaHostProviderFromHwnd 允许你"拦截"对特定窗口的 UI Automation 查询,并用你自己的实现来响应这些查询。这是一种非侵入式的方法,可以为不支持 UI Automation 的应用程序添加辅助功能支持,而无需修改原始程序。
如何实现
意思是目标程序本身是需要提供这个对外的 UI 信息访问,然后我自己实现的 Provider 实际上就是对这个本来就能采集到的信息转换成 UI Automation 能识别的信息吗
你的理解接近了真相,但还需要一些澄清。让我详细解释一下:
-
目标程序的基本信息:
- 所有 Windows 程序,即使没有专门实现 UI Automation,也会通过 Windows API 提供一些基本的 UI 信息。
- 这些信息包括窗口标题、位置、大小、可见性等。
-
Windows 消息系统:
- Windows 使用消息系统来管理窗口交互。
- 即使程序没有实现 UI Automation,它也会响应某些标准的 Windows 消息。
-
你的自定义 Provider 的角色:
- 你的 Provider 可以利用这些基本信息和标准消息来构建更丰富的 UI Automation 接口。
- 它不仅仅是简单地转换已有信息,而是可以通过各种方法来"推断"或"模拟"更多的信息。
-
信息获取方法:
- 使用 Windows API 函数(如 GetWindowText, GetWindowRect 等)获取基本信息。
- 发送 Windows 消息(如 WM_GETTEXT)来获取更多细节。
- 分析窗口的视觉结构来推断控件类型和层次。
-
增强和扩展:
- 你的 Provider 可以添加原本不存在的功能,比如为不支持键盘导航的程序添加这个功能。
- 可以实现更高级的模式,如模式识别来猜测控件的用途。
-
限制:
- 你无法直接访问目标程序的内部数据结构或逻辑。
- 某些复杂的交互或自定义控件可能难以准确模拟。
示例:
假设有一个简单的旧式文本编辑器,它没有实现 UI Automation。你的 Provider 可能会这样工作:
class TextEditorProvider : public IRawElementProviderSimple
{
public:
HRESULT GetPropertyValue(PROPERTYID propertyId, VARIANT *pRetVal) override
{
switch (propertyId)
{
case UIA_NamePropertyId:
// 使用 Windows API 获取窗口标题
TCHAR title[256];
GetWindowText(m_hwnd, title, 256);
pRetVal->bstrVal = SysAllocString(title);
pRetVal->vt = VT_BSTR;
return S_OK;
case UIA_BoundingRectanglePropertyId:
// 获取窗口位置和大小
RECT rect;
GetWindowRect(m_hwnd, &rect);
// 填充 UiaRect 结构...
return S_OK;
case UIA_IsTextPatternAvailablePropertyId:
// 假设这是一个文本编辑器,我们声明它支持文本模式
pRetVal->boolVal = VARIANT_TRUE;
pRetVal->vt = VT_BOOL;
return S_OK;
}
return E_NOTIMPL;
}
// 实现 ITextProvider 接口来提供文本访问能力
HRESULT GetText(int startOffset, int endOffset, BSTR *pRetVal) override
{
// 发送 WM_GETTEXT 消息来获取文本内容
int length = SendMessage(m_hwnd, WM_GETTEXTLENGTH, 0, 0);
std::vector<TCHAR> buffer(length + 1);
SendMessage(m_hwnd, WM_GETTEXT, length + 1, (LPARAM)buffer.data());
// 处理 startOffset 和 endOffset...
*pRetVal = SysAllocString(buffer.data());
return S_OK;
}
private:
HWND m_hwnd; // 目标窗口的句柄
};
在这个例子中:
- Provider 使用标准 Windows API 获取基本窗口信息。
- 它假设窗口是文本编辑器,并声明支持文本模式。
- 通过发送 Windows 消息来获取文本内容,即使程序本身没有实现 UI Automation。
总结: 你的 Provider 不仅仅是转换已有信息,而是主动使用各种 Windows API 和消息来获取信息,并将这些基本信息转化为更丰富的 UI Automation 接口。它可以添加原程序没有的功能,但也受限于无法直接访问程序内部状态。这种方法允许你为旧程序或第三方应用添加辅助功能支持,而无需修改原始代码。
如何分析程序的 UI 信息
你提出了一个非常深刻的问题,这触及了 UI 自动化和应用程序分析的核心。让我详细解释一下这个问题:
- 基本 Windows 信息
所有 Windows 程序,无论是否支持 UI Automation,都会通过 Windows API 提供一些基本信息:
- 窗口句柄 (HWND)
- 窗口类名
- 窗口标题
- 窗口位置和大小
- 子窗口和控件
这些信息可以通过标准 Windows API 函数获取,如 GetWindowText(), GetClassName(), GetWindowRect() 等。
- 控件类型推断
虽然程序可能不直接支持 UI Automation,但我们可以通过分析窗口类名和其他属性来推断控件类型:
- "Button" 类通常是按钮
- "Edit" 类通常是文本框
- "ListBox" 类通常是列表框
- 消息响应分析
通过发送 Windows 消息并分析响应,我们可以推断控件的功能:
- 发送 WM_GETTEXT 消息可以获取控件的文本内容
- 发送 WM_GETTEXTLENGTH 可以获取文本长度
- 尝试 EM_GETSEL 消息可以检测是否支持文本选择
- 视觉分析
通过分析窗口的视觉结构,我们可以推断 UI 元素的层次和关系:
- 使用 GetWindowRect() 和 GetClientRect() 分析控件的位置和大小
- 分析子窗口的排列可以推断出布局结构(如表格、列表等)
- 行为分析
通过模拟用户操作并观察结果,我们可以推断控件的行为:
- 发送鼠标点击消息并观察窗口变化
- 尝试设置焦点并观察响应
- 启发式方法
基于常见的 UI 模式和最佳实践,我们可以做出一些合理的假设:
- 窗口顶部的子窗口可能是菜单栏
- 窗口底部的进度条可能表示某种进度
- 特定位置的图标可能代表特定功能(如关闭、最小化按钮)
- 工具和技术
有一些工具和技术可以帮助分析应用程序的 UI 结构:
- Spy++ (包含在 Visual Studio 中) 可以查看窗口层次和属性
- Windows SDK 中的 Inspect.exe 工具
- 使用 Windows Hook 技术来拦截和分析消息
示例代码:
以下是一个简单的例子,展示如何获取基本窗口信息并推断一些 UI 元素:
#include <windows.h>
#include <iostream>
#include <string>
void AnalyzeWindow(HWND hwnd) {
char className[256];
char windowText[256];
RECT rect;
GetClassName(hwnd, className, 256);
GetWindowText(hwnd, windowText, 256);
GetWindowRect(hwnd, &rect);
std::cout << "Class: " << className << std::endl;
std::cout << "Text: " << windowText << std::endl;
std::cout << "Position: (" << rect.left << "," << rect.top << ") - ("
<< rect.right << "," << rect.bottom << ")" << std::endl;
// 推断控件类型
if (strcmp(className, "Button") == 0) {
std::cout << "This appears to be a button." << std::endl;
} else if (strcmp(className, "Edit") == 0) {
std::cout << "This appears to be a text box." << std::endl;
// 检查是否支持文本选择
LRESULT result = SendMessage(hwnd, EM_GETSEL, 0, 0);
if (result != -1) {
std::cout << "This text box supports text selection." << std::endl;
}
}
// 检查子窗口
HWND child = GetWindow(hwnd, GW_CHILD);
if (child) {
std::cout << "This window has child windows." << std::endl;
}
}
int main() {
HWND hwnd = FindWindow(NULL, "Notepad"); // 查找记事本窗口
if (hwnd) {
AnalyzeWindow(hwnd);
} else {
std::cout << "Notepad window not found." << std::endl;
}
return 0;
}
这个例子展示了如何获取基本窗口信息,并做一些简单的推断。在实际应用中,你可能需要更复杂的逻辑和更多的启发式方法来准确分析 UI 结构。
总结: 虽然目标程序可能不直接支持 UI Automation,但通过综合使 用 Windows API、消息系统、视觉分析和启发式方法,我们可以收集足够的信息来构建一个合理的 UI 模型。这种方法虽然不如直接支持 UI Automation 的程序那样精确和全面,但对于许多自动化和辅助技术应用来说已经足够有用了。
可以使用的 UI 分析工具
是的,有几种方法可以分析应用程序窗口内的子UI元素。以下是一些常用的技术:
- 枚举子窗口
使用 Windows API 函数 EnumChildWindows() 可以枚举一个窗口的所有直接子窗口。
BOOL CALLBACK EnumChildProc(HWND hwnd, LPARAM lParam) {
char className[256];
char windowText[256];
GetClassName(hwnd, className, 256);
GetWindowText(hwnd, windowText, 256);
printf("Child Window - Class: %s, Text: %s\n", className, windowText);
return TRUE;
}
// 使用
EnumChildWindows(parentHwnd, EnumChildProc, 0);
- 递归枚举
通过递归调用,可以枚举出窗口的整个子窗口树结构。
void EnumChildWindowsRecursive(HWND hwnd, int level = 0) {
char className[256];
char windowText[256];
GetClassName(hwnd, className, 256);
GetWindowText(hwnd, windowText, 256);
printf("%*s%s: %s\n", level*2, "", className, windowText);
// 递归枚举子窗口
HWND child = GetWindow(hwnd, GW_CHILD);
while (child) {
EnumChildWindowsRecursive(child, level + 1);
child = GetWindow(child, GW_HWNDNEXT);
}
}
- 使 用 Accessibility API
Windows 提供了 Accessibility API,可以获取更详细的UI元素信息,包括那些不是窗口的UI元素。
#include <oleacc.h>
void EnumAccessibleChildren(IAccessible* pAcc, int level = 0) {
long childCount;
pAcc->get_accChildCount(&childCount);
for (long i = 1; i <= childCount; i++) {
VARIANT varChild;
varChild.vt = VT_I4;
varChild.lVal = i;
IDispatch* pDisp;
pAcc->get_accChild(varChild, &pDisp);
if (pDisp) {
IAccessible* pChildAcc;
pDisp->QueryInterface(IID_IAccessible, (void**)&pChildAcc);
if (pChildAcc) {
BSTR bstrName;
pChildAcc->get_accName(varChild, &bstrName);
VARIANT varRole;
pChildAcc->get_accRole(varChild, &varRole);
printf("%*sName: %S, Role: %d\n", level*2, "", bstrName, varRole.lVal);
SysFreeString(bstrName);
EnumAccessibleChildren(pChildAcc, level + 1);
pChildAcc->Release();
}
pDisp->Release();
}
}
}
// 使用
IAccessible* pAcc;
HRESULT hr = AccessibleObjectFromWindow(hwnd, OBJID_CLIENT, IID_IAccessible, (void**)&pAcc);
if (SUCCEEDED(hr)) {
EnumAccessibleChildren(pAcc);
pAcc->Release();
}
- 使用 UI Automation API
对于支持 UI Automation 的应用程序,可以使用 UI Automation API 来获取更丰富的UI元素信息。
#include <UIAutomation.h>
void EnumUiAutomationElements(IUIAutomationElement* pElement, int level = 0) {
BSTR name;
pElement->get_CurrentName(&name);
CONTROLTYPEID controlType;
pElement->get_CurrentControlType(&controlType);
printf("%*sName: %S, ControlType: %d\n", level*2, "", name, controlType);
SysFreeString(name);
IUIAutomationTreeWalker* pTreeWalker;
g_pAutomation->get_ControlViewWalker(&pTreeWalker);
IUIAutomationElement* pChild;
pTreeWalker->GetFirstChildElement(pElement, &pChild);
while (pChild) {
EnumUiAutomationElements(pChild, level + 1);
IUIAutomationElement* pNext;
pTreeWalker->GetNextSiblingElement(pChild, &pNext);
pChild->Release();
pChild = pNext;
}
pTreeWalker->Release();
}
// 使用
IUIAutomation* g_pAutomation;
CoCreateInstance(CLSID_CUIAutomation, NULL, CLSCTX_INPROC_SERVER,
IID_IUIAutomation, (void**)&g_pAutomation);
IUIAutomationElement* pRoot;
g_pAutomation->ElementFromHandle(hwnd, &pRoot);
EnumUiAutomationElements(pRoot);
pRoot->Release();
g_pAutomation->Release();
- 使用第三方工具
有一些第三方工具可以帮助分析应用程序的UI结构,例如:
- Spy++ (包含在Visual Studio中)
- Inspect.exe (Windows SDK工具)
- UISpy (开源工具)
这 些方法各有优缺点:
- 枚举子窗口方法简单,但只能获取窗口级别的元素。
- Accessibility API 可以获取更多元素,但可能不够详细。
- UI Automation API 提供最丰富的信息,但并非所有应用程序都完全支持它。
在实际应用中,你可能需要结合使用这些方法来获取最全面的UI元素信息。对于不同的目标应用程序,可能需要尝试不同的方法来找出最有效的分析技术。